<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - postprocessing with nodes</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Node-Based Post-Processing
		</div>

		<script type="module">

			import * as THREE from '../build/three.module.js';

			import { GUI } from './jsm/libs/dat.gui.module.js';
			import * as Nodes from './jsm/nodes/Nodes.js';

			let camera, scene, renderer;
			let object, light, nodepost;
			let gui;

			const clock = new THREE.Clock();
			const frame = new Nodes.NodeFrame();

			const param = { example: new URL( window.location.href ).searchParams.get( 'e' ) || 'color-adjustment' };

			const textureLoader = new THREE.TextureLoader();

			const lensflare2 = textureLoader.load( 'textures/lensflare/lensflare0.png' );
			lensflare2.wrapS = lensflare2.wrapT = THREE.RepeatWrapping;

			const decalNormal = textureLoader.load( 'textures/decal/decal-normal.jpg' );
			decalNormal.wrapS = decalNormal.wrapT = THREE.RepeatWrapping;

			init();
			animate();

			function clearGui() {

				if ( gui ) gui.destroy();

				gui = new GUI();

				gui.add( param, 'example', {
					'basic / color-adjustment': 'color-adjustment',
					'basic / blends': 'blends',
					'basic / fade': 'fade',
					'basic / invert': 'invert',
					'basic / blur': 'blur',
					'adv / motion-blur': 'motion-blur',
					'adv / saturation': 'saturation',
					'adv / refraction': 'refraction',
					'adv / mosaic': 'mosaic'
				} ).onFinishChange( function () {

					updateMaterial();

				} );

				gui.open();

			}

			function addGui( name, value, callback, isColor, min, max ) {

				let node;

				param[ name ] = value;

				if ( isColor ) {

					node = gui.addColor( param, name ).onChange( function () {

						callback( param[ name ] );

					} );

				} else if ( typeof value == 'object' ) {

					param[ name ] = value[ Object.keys( value )[ 0 ] ];

					node = gui.add( param, name, value ).onChange( function () {

						callback( param[ name ] );

					} );

				} else {

					node = gui.add( param, name, min, max ).onChange( function () {

						callback( param[ name ] );

					} );

				}

				return node;

			}

			function updateMaterial() {

				const name = param.example;

				let screen, fade, scale, size;

				clearGui();

				switch ( name ) {

					case 'color-adjustment':

						// POST

						screen = new Nodes.ScreenNode();

						const hue = new Nodes.FloatNode();
						const sataturation = new Nodes.FloatNode( 1 );
						const vibrance = new Nodes.FloatNode();
						const brightness = new Nodes.FloatNode( 0 );
						const contrast = new Nodes.FloatNode( 1 );

						const hueNode = new Nodes.ColorAdjustmentNode( screen, hue, Nodes.ColorAdjustmentNode.HUE );
						const satNode = new Nodes.ColorAdjustmentNode( hueNode, sataturation, Nodes.ColorAdjustmentNode.SATURATION );
						const vibranceNode = new Nodes.ColorAdjustmentNode( satNode, vibrance, Nodes.ColorAdjustmentNode.VIBRANCE );
						const brightnessNode = new Nodes.ColorAdjustmentNode( vibranceNode, brightness, Nodes.ColorAdjustmentNode.BRIGHTNESS );
						const contrastNode = new Nodes.ColorAdjustmentNode( brightnessNode, contrast, Nodes.ColorAdjustmentNode.CONTRAST );

						nodepost.output = contrastNode;

						// GUI

						addGui( 'hue', hue.value, function ( val ) {

							hue.value = val;

						}, false, 0, Math.PI * 2 );

						addGui( 'saturation', sataturation.value, function ( val ) {

							sataturation.value = val;

						}, false, 0, 2 );

						addGui( 'vibrance', vibrance.value, function ( val ) {

							vibrance.value = val;

						}, false, - 1, 1 );

						addGui( 'brightness', brightness.value, function ( val ) {

							brightness.value = val;

						}, false, 0, .5 );

						addGui( 'contrast', contrast.value, function ( val ) {

							contrast.value = val;

						}, false, 0, 2 );

						break;

					case 'fade':

						// POST

						const color = new Nodes.ColorNode( 0xFFFFFF );
						const percent = new Nodes.FloatNode( .5 );

						fade = new Nodes.MathNode(
							new Nodes.ScreenNode(),
							color,
							percent,
							Nodes.MathNode.MIX
						);

						nodepost.output = fade;

						// GUI

						addGui( 'color', color.value.getHex(), function ( val ) {

							color.value.setHex( val );

						}, true );

						addGui( 'fade', percent.value, function ( val ) {

							percent.value = val;

						}, false, 0, 1 );

						break;

					case 'invert':

						// POST

						const alpha = new Nodes.FloatNode( 1 );

						screen = new Nodes.ScreenNode();
						const inverted = new Nodes.MathNode( screen, Nodes.MathNode.INVERT );

						fade = new Nodes.MathNode(
							screen,
							inverted,
							alpha,
							Nodes.MathNode.MIX
						);

						nodepost.output = fade;

						// GUI

						addGui( 'alpha', alpha.value, function ( val ) {

							alpha.value = val;

						}, false, 0, 1 );

						break;

					case 'blends':

						// POST

						const multiply = new Nodes.OperatorNode(
							new Nodes.ScreenNode(),
							new Nodes.TextureNode( lensflare2 ),
							Nodes.OperatorNode.ADD
						);

						nodepost.output = multiply;

						// GUI

						addGui( 'blend', {
							'addition': Nodes.OperatorNode.ADD,
							'subtract': Nodes.OperatorNode.SUB,
							'multiply': Nodes.OperatorNode.MUL,
							'division': Nodes.OperatorNode.DIV
						}, function ( val ) {

							multiply.op = val;

							nodepost.needsUpdate = true;

						} );

						break;

					case 'saturation':

						// PASS

						screen = new Nodes.ScreenNode();
						const sat = new Nodes.FloatNode( 0 );

						const satrgb = new Nodes.FunctionNode( [
							"vec3 satrgb( vec3 rgb, float adjustment ) {",
							// include luminance function from LuminanceNode
							"	vec3 intensity = vec3( luminance( rgb ) );",
							"	return mix( intensity, rgb, adjustment );",
							"}"
						].join( "\n" ), [ Nodes.LuminanceNode.Nodes.luminance ] );

						const saturation = new Nodes.FunctionCallNode( satrgb );
						saturation.inputs.rgb = screen;
						saturation.inputs.adjustment = sat;

						nodepost.output = saturation;

						// GUI

						addGui( 'saturation', sat.value, function ( val ) {

							sat.value = val;

						}, false, 0, 2 );

						break;

					case 'refraction':

						// POST

						const normal = new Nodes.TextureNode( decalNormal );
						const normalXY = new Nodes.SwitchNode( normal, 'xy' );
						scale = new Nodes.FloatNode( .5 );

						const normalXYFlip = new Nodes.MathNode(
							normalXY,
							Nodes.MathNode.INVERT
						);

						const offsetNormal = new Nodes.OperatorNode(
							normalXYFlip,
							new Nodes.FloatNode( .5 ),
							Nodes.OperatorNode.ADD
						);

						const scaleTexture = new Nodes.OperatorNode(
							new Nodes.SwitchNode( normal, 'z' ),
							offsetNormal,
							Nodes.OperatorNode.MUL
						);

						const scaleNormal = new Nodes.MathNode(
							new Nodes.FloatNode( 1 ),
							scaleTexture,
							scale,
							Nodes.MathNode.MIX
						);

						const offsetCoord = new Nodes.OperatorNode(
							new Nodes.UVNode(),
							scaleNormal,
							Nodes.OperatorNode.MUL
						);

						screen = new Nodes.ScreenNode( offsetCoord );

						nodepost.output = screen;

						// GUI

						addGui( 'scale', scale.value, function ( val ) {

							scale.value = val;

						}, false, 0, 1 );

						addGui( 'invert', false, function ( val ) {

							offsetNormal.a = val ? normalXYFlip : normalXY;

							nodepost.needsUpdate = true;

						} );

						break;

					case 'motion-blur':

						// POST

						size = renderer.getDrawingBufferSize( new THREE.Vector2() );

						screen = new Nodes.ScreenNode();

						const previousFrame = new Nodes.RTTNode( size.width, size.height, screen );

						const motionBlur = new Nodes.MathNode(
							previousFrame,
							screen,
							new Nodes.FloatNode( .5 ),
							Nodes.MathNode.MIX
						);

						const currentFrame = new Nodes.RTTNode( size.width, size.height, motionBlur );
						currentFrame.saveTo = previousFrame;

						nodepost.output = currentFrame;

						break;

					case 'mosaic':

						// POST

						scale = new Nodes.FloatNode( 128 );
						fade = new Nodes.FloatNode( 1 );
						const uv = new Nodes.UVNode();

						const blocks = new Nodes.OperatorNode(
							uv,
							scale,
							Nodes.OperatorNode.MUL
						);

						const blocksSize = new Nodes.MathNode(
							blocks,
							Nodes.MathNode.FLOOR
						);

						const mosaicUV = new Nodes.OperatorNode(
							blocksSize,
							scale,
							Nodes.OperatorNode.DIV
						);

						const fadeScreen = new Nodes.MathNode(
							uv,
							mosaicUV,
							fade,
							Nodes.MathNode.MIX
						);

						nodepost.output = new Nodes.ScreenNode( fadeScreen );

						// GUI

						addGui( 'scale', scale.value, function ( val ) {

							scale.value = val;

						}, false, 16, 1024 );

						addGui( 'fade', fade.value, function ( val ) {

							fade.value = val;

						}, false, 0, 1 );

						addGui( 'mask', false, function ( val ) {

							fadeScreen.c = val ? new Nodes.TextureNode( lensflare2 ) : fade;

							nodepost.needsUpdate = true;

						} );

						break;

					case 'blur':

						// POST

						size = renderer.getDrawingBufferSize( new THREE.Vector2() );

						const blurScreen = new Nodes.BlurNode( new Nodes.ScreenNode() );
						blurScreen.size = new THREE.Vector2( size.width, size.height );

						nodepost.output = blurScreen;

						// GUI

						addGui( 'blurX', blurScreen.radius.x, function ( val ) {

							blurScreen.radius.x = val;

						}, false, 0, 15 );

						addGui( 'blurY', blurScreen.radius.y, function ( val ) {

							blurScreen.radius.y = val;

						}, false, 0, 15 );

						break;

				}

				nodepost.needsUpdate = true;

				// test serialization
				/*
							let library = {};
							library[ lensflare2.uuid ] = lensflare2;
							library[ decalNormal.uuid ] = decalNormal;

							let json = nodepost.toJSON();

							nodepost.output = new NodeMaterialLoader( null, library ).parse( json ).value;
						*/

			}

			function init() {

				renderer = new THREE.WebGLRenderer();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				nodepost = new Nodes.NodePostProcessing( renderer );

				//

				camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 );
				camera.position.z = 400;

				scene = new THREE.Scene();
				scene.fog = new THREE.Fog( 0x0066FF, 1, 1000 );

				object = new THREE.Object3D();
				scene.add( object );

				const geometry = new THREE.SphereGeometry( 1, 4, 4 );

				for ( let i = 0; i < 100; i ++ ) {

					const material = new THREE.MeshPhongMaterial( { color: 0x888888 + ( Math.random() * 0x888888 ), flatShading: true } );
					const mesh = new THREE.Mesh( geometry, material );
					mesh.position.set( Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 ).normalize();
					mesh.position.multiplyScalar( Math.random() * 400 );
					mesh.rotation.set( Math.random() * 2, Math.random() * 2, Math.random() * 2 );
					mesh.scale.x = mesh.scale.y = mesh.scale.z = 10 + ( Math.random() * 40 );
					object.add( mesh );

				}

				scene.add( new THREE.AmbientLight( 0x999999 ) );

				light = new THREE.DirectionalLight( 0xffffff );
				light.position.set( 1, 1, 1 );
				scene.add( light );

				//

				updateMaterial();

				window.addEventListener( 'resize', onWindowResize );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				nodepost.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				requestAnimationFrame( animate );

				const delta = clock.getDelta();

				object.rotation.x += 0.005;
				object.rotation.y += 0.01;

				frame.update( delta );

				nodepost.render( scene, camera, frame );

			}


		</script>

	</body>
</html>
